package animation;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * An instance of an animation tree.
 */
public class AnimationTreeInstance {
	// links the animation tree up with the set of animations
	public AnimationTreeInstance(AnimationTree animationTree, Map<String, AnimationInstance> animations) {
		if (animationTree == null) {
			throw new IllegalArgumentException("Animation tree not specified");
		}
		this.animationTree = animationTree;
		nodes = new ArrayList<Node>();
		nodeIndexMap = new HashMap<String, Integer>();

		if (animationTree.getRoot() == null) {
			return;
		}

		// recursively assign indices to nodes
		nodes.add(null);
		assignNodes(animationTree.getRoot(), 0, animations);

		// determine animation evaluator stack depth
		evaluatorDepthRequirement = computeDepth(0);
	}

	// returns number of nodes assigned
	private void assignNodes(AnimationTree.Node node, int index, Map<String, AnimationInstance> animations) {
		nodeIndexMap.put(node.getName(), index);
		switch (node.getType()) {
			case ANIMATION:
			{
				AnimationTree.AnimationNode n = (AnimationTree.AnimationNode) node;
				AnimationInstance animation = animations.get(n.getAnimation());
				if (animation == null) {
					throw new IllegalArgumentException("Animation " + n.getAnimation() + " not found");
				}

				AnimationNode newNode = new AnimationNode(n, animation);
				nodes.set(index, newNode);
				break;
			}
			case SELECT:
			{
				AnimationTree.SelectNode n = (AnimationTree.SelectNode) node;
				SelectNode newNode = new SelectNode(n, nodes.size());
				nodes.set(index, newNode);

				int childIndex = nodes.size();
				for (int i = 0; i < n.getChildCount(); ++i) {
					// push null spaces in array for children
					nodes.add(null);
				}
				for (int i = 0; i < n.getChildCount(); ++i) {
					assignNodes(n.getChild(i), childIndex + i, animations);
				}
				break;
			}
			case BLEND:
			{
				AnimationTree.BlendNode n = (AnimationTree.BlendNode) node;
				BlendNode newNode = new BlendNode(n, nodes.size());
				nodes.set(index, newNode);

				int childIndex = nodes.size();
				for (int i = 0; i < n.getChildCount(); ++i) {
					// push null spaces in array for children
					nodes.add(null);
				}
				for (int i = 0; i < n.getChildCount(); ++i) {
					assignNodes(n.getChild(i), childIndex + i, animations);
				}
				break;
			}
			case SEQUENCE:
			{
				AnimationTree.SequenceNode n = (AnimationTree.SequenceNode) node;
				SequenceNode newNode = new SequenceNode(n, nodes.size());
				nodes.set(index, newNode);

				int childIndex = nodes.size();
				for (int i = 0; i < n.getChildCount(); ++i) {
					// push null spaces in array for children
					nodes.add(null);
				}
				for (int i = 0; i < n.getChildCount(); ++i) {
					assignNodes(n.getChild(i), childIndex + i, animations);
				}
				break;
			}
			default:
				throw new IllegalArgumentException("Unknown node type " + node.getType());
		}
	}

	private int computeDepth(int nodeIndex) {
		Node node = nodes.get(nodeIndex);
		switch (node.getNode().getType()) {
			case ANIMATION:
				// animation nodes increase depth by 1
				return 1;
			case SELECT:
			{
				// find depth of all children
				int maxDepth = 0;
				SelectNode typeNode = (SelectNode) node;
				AnimationTree.SelectNode treeNode = (AnimationTree.SelectNode) node.getNode();
				for (int c = 0; c < treeNode.getChildCount(); ++c) {
					// find the child with the largest depth
					maxDepth = Math.max(maxDepth, computeDepth(typeNode.getChildNodeIndex(c)));
				}
				// select nodes don't increase depth since they just pass through
				return maxDepth;
			}
			case BLEND:
			{
				// find depth of all children
				int maxDepth = 0;
				BlendNode typeNode = (BlendNode) node;
				AnimationTree.BlendNode treeNode = (AnimationTree.BlendNode) node.getNode();
				for (int c = 0; c < treeNode.getChildCount(); ++c) {
					// find the child with the largest depth
					maxDepth = Math.max(maxDepth, computeDepth(typeNode.getChildNodeIndex(c)));
				}
				// blend nodes increase depth by 2, since they need an intermediate buffer
				return maxDepth + 2;
			}
			case SEQUENCE:
			{
				// find depth of all children
				int maxDepth = 0;
				SequenceNode typeNode = (SequenceNode) node;
				AnimationTree.SequenceNode treeNode = (AnimationTree.SequenceNode) node.getNode();
				for (int c = 0; c < treeNode.getChildCount(); ++c) {
					// find the child with the largest depth
					maxDepth = Math.max(maxDepth, computeDepth(typeNode.getChildNodeIndex(c)));
				}
				// sequence nodes increase depth by 1, since they blend with the current buffer
				return maxDepth + 1;
			}
			default:
				throw new IllegalArgumentException("Unknown node type " + node.getNode().getType());
		}
	}

	// holds data about nodes in the animation tree
	// for nodes with children, provides direct access to child node indices
	// for animation nodes, provides direct pointer to AnimationInstance
	public abstract static class Node {
		private Node(AnimationTree.Node node) {
			this.node = node;
		}

		public AnimationTree.Node getNode() {
			return node;
		}

		private AnimationTree.Node node;

		protected abstract State.NodeState createNodeState();
	}

	public static class AnimationNode extends Node {
		private AnimationNode(AnimationTree.AnimationNode node, AnimationInstance animation) {
			super(node);
			this.animation = animation;
		}

		public AnimationInstance getAnimation() {
			return animation;
		}

		private AnimationInstance animation;

		protected State.NodeState createNodeState() {
			return new State.AnimationNodeState();
		}
	}

	public static class SelectNode extends Node {
		private SelectNode(AnimationTree.SelectNode node, int firstChild) {
			super(node);
			this.firstChild = firstChild;
		}

		public int getChildNodeIndex(int i) {
			return firstChild + i;
		}

		private int firstChild;

		protected State.NodeState createNodeState() {
			return new State.SelectNodeState();
		}
	}

	public static class BlendNode extends Node {
		private BlendNode(AnimationTree.BlendNode node, int firstChild) {
			super(node);
			this.firstChild = firstChild;
		}

		public int getChildNodeIndex(int i) {
			return firstChild + i;
		}

		private int firstChild;

		protected State.NodeState createNodeState() {
			return new State.BlendNodeState();
		}
	}

	public static class SequenceNode extends Node {
		private SequenceNode(AnimationTree.SequenceNode node, int firstChild) {
			super(node);
			this.firstChild = firstChild;
		}

		public int getChildNodeIndex(int i) {
			return firstChild + i;
		}

		private int firstChild;

		protected State.NodeState createNodeState() {
			return new State.SequenceNodeState(((AnimationTree.SequenceNode) getNode()).getChildCount());
		}
	}

	public AnimationTree getAnimationTree() {
		return animationTree;
	}

	public Node getNode(int i) {
		return nodes.get(i);
	}

	public int getNodeIndex(String name) {
		Integer idx = nodeIndexMap.get(name);
		return (idx == null) ? -1 : idx;
	}

	// when evaluating an animation tree, each tree has a stack depth requirement
	// this value is computed once and stored in the tree
	public int getEvaluatorDepthRequirement() {
		return evaluatorDepthRequirement;
	}

	private AnimationTree animationTree;
	// flat array of tree nodes
	private ArrayList<Node> nodes;
	// maps node names to indices
	private HashMap<String, Integer> nodeIndexMap;
	// stack depth required for evaluator
	private int evaluatorDepthRequirement;


	////////
	// state

	public State createState() {
		return new State(this);
	}

	public static class State {
		private State(AnimationTreeInstance instance) {
			this.instance = instance;

			nodeStates = new ArrayList<NodeState>();
			nodeStates.ensureCapacity(instance.nodes.size());
			// create node states
			for (Node n : instance.nodes) {
				nodeStates.add(n.createNodeState());
			}
		}

		public AnimationTreeInstance getAnimationTreeInstance() {
			return instance;
		}

		// the state of a node in the tree
		public static abstract class NodeState {
			protected NodeState() {
			}

			protected abstract void update(AnimationTreeInstance instance, int node, float dt);
		}

		public static class AnimationNodeState extends NodeState {
			private AnimationNodeState() {
			}

			public float getTime() {
				return time;
			}

			public void setTime(float time) {
				this.time = time;
			}

			public float getPlaySpeed() {
				return playSpeed;
			}

			public void setPlaySpeed(float playSpeed) {
				this.playSpeed = playSpeed;
			}

			public boolean getLoop() {
				return loop;
			}

			public void setLoop(boolean loop) {
				this.loop = loop;
			}

			private float time;         // current time of animation
			private float playSpeed;    // play speed of animation
			private boolean loop;       // whether animation is looping

			protected void update(AnimationTreeInstance instance, int node, float dt) {
				if (playSpeed == 0.0f) {
					return;
				}

				// obtain the animation length so we can clamp or wrap
				float animationLength = ((AnimationNode) instance.nodes.get(node)).getAnimation().getAnimation().getLength();
				time += playSpeed * dt;
				if (loop) {
					time = (animationLength == 0.0f) ? 0.0f : time % animationLength;
					if (time < 0.0f) {
						time += animationLength;
					}
				} else {
					// clamp time
					time = Math.max(0.0f, Math.min(animationLength, time));
				}
			}
		}

		public static class SelectNodeState extends NodeState {
			private SelectNodeState() {
			}

			public int getIndex() {
				return index;
			}

			public void setIndex(int index) {
				this.index = index;
			}

			private int index;  // current index of node

			protected void update(AnimationTreeInstance instance, int node, float dt) {
				// no update required
			}
		}

		public static class BlendNodeState extends NodeState {
			private BlendNodeState() {
			}

			public float getWeight() {
				return weight;
			}

			public void setWeight(float weight) {
				this.weight = weight;
			}

			public float getTargetWeight() {
				return targetWeight;
			}

			public void setTargetWeight(float targetWeight) {
				this.targetWeight = targetWeight;
			}

			public float getTransitionSpeed() {
				return transitionSpeed;
			}

			public void setTransitionSpeed(float transitionSpeed) {
				this.transitionSpeed = transitionSpeed;
			}

			private float weight;           // current weight of node
			private float targetWeight;     // target weight of node
			private float transitionSpeed;  // weight transition speed

			protected void update(AnimationTreeInstance instance, int node, float dt) {
				if (weight < targetWeight) {
					weight = Math.min(weight + transitionSpeed * dt, targetWeight);
				} else if (weight > targetWeight) {
					weight = Math.max(weight - transitionSpeed * dt, targetWeight);
				}
			}
		}

		public static class SequenceNodeState extends NodeState {
			private SequenceNodeState(int count) {
				weights = new float[count];
				targetWeights = new float[count];
				transitionSpeeds = new float[count];
			}

			public float getWeight(int i) {
				return weights[i];
			}

			public void setWeight(int i, float weight) {
				weights[i] = weight;
			}

			public float getTargetWeight(int i) {
				return targetWeights[i];
			}

			public void setTargetWeight(int i, float targetWeight) {
				targetWeights[i] = targetWeight;
			}

			public float getTransitionSpeed(int i) {
				return transitionSpeeds[i];
			}

			public void setTransitionSpeed(int i, float transitionSpeed) {
				transitionSpeeds[i] = transitionSpeed;
			}

			private float[] weights;            // current weight of node for each child
			private float[] targetWeights;      // target weight of node for each child
			private float[] transitionSpeeds;   // weight transition speed for each child

			protected void update(AnimationTreeInstance instance, int node, float dt) {
				for (int i = 0; i < weights.length; ++i) {
					if (weights[i] < targetWeights[i]) {
						weights[i] = Math.min(weights[i] + transitionSpeeds[i] * dt, targetWeights[i]);
					} else if (weights[i] > targetWeights[i]) {
						weights[i] = Math.max(weights[i] - transitionSpeeds[i] * dt, targetWeights[i]);
					}
				}
			}
		}

		public NodeState getNodeState(int i) {
			return nodeStates.get(i);
		}

		public void update(float dt) {
			for (int i = 0;i < nodeStates.size(); ++i) {
				nodeStates.get(i).update(instance, i, dt);
			}
		}

		private AnimationTreeInstance instance;
		private ArrayList<NodeState> nodeStates;
	}
}
